/*
 * MIT License
 * 
 * Copyright (c) 2016 wen.gu <454727014@qq.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/***************************************************************************
* Name: tiny_ini_file.c
*
* Purpose: ini file operation API implementation
*
* Developer:
*   wen.gu , 2016-08-10
*
* TODO:
*
***************************************************************************/
#define _CRT_SECURE_NO_WARNINGS

#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>

#include "tiny_ini_file.h"


/***************************************************************************
*
* macro define
*
***************************************************************************/
#define LOGI printf
#define  LOGE printf
/***************************************************************************
*
* data structure define
*
***************************************************************************/
struct key_value_pair_s
{
    char* key;
    char* value;
    void* opaque; /** private data, user cann't modify it */
};

/** ini file parser */
typedef struct my_tiny_ini_file_s
{
    tiny_ini_file_t common;

    FILE* fp; /** ini file handle */
    ini_section_t* sections;

    GU32 section_count;
}my_ini_file_t;

/***************************************************************************
*
* API define
*
***************************************************************************/

static inline GBOL isCommentTag(char ch)
{
    return (ch == '#' || ch == ';') ? GTRUE : GFALSE;
}

static ini_section_t* iniFileNewSection(const char* name)
{
    ini_section_t* isec = NULL;
    GU32 len = strlen(name);
    char* buf = (char*)malloc(sizeof(ini_section_t) + len + 1);
    

    if (buf)
    {
        isec = (ini_section_t*)buf;
        memset(isec, 0, sizeof(ini_section_t));

        isec->name = buf + sizeof(ini_section_t);

        memcpy(isec->name, name, len);
        isec->name[len] = '\0';
    }
    else
    {
        LOGE("alloc section instance failed\n");
    }

    return isec;
}

static key_value_pair_t* iniFileNewParameter(char* key, GU32 key_len,
    char* value, GU32 value_len)
{
    key_value_pair_t* param = NULL;
    GU32 totalSize = key_len + value_len;
    char* buf = (char*)malloc(totalSize + sizeof(key_value_pair_t)+2);

    if (buf)
    {
        param = (key_value_pair_t*)buf;

        buf += sizeof(key_value_pair_t);

        param->key = buf;
        memcpy(buf, key, key_len);
        buf[key_len] = '\0';

        param->value = buf + key_len + 1;
        memcpy(param->value, value, value_len);

        buf[totalSize + 1] = '\0';

        param->opaque = NULL;
    }
    else
    {
        LOGE("alloc buffer for key and value failed\n");

    }

    return param;
}

static GS32 iniFileAddParameter(ini_section_t* section, key_value_pair_t* param)
{

    key_value_pair_t* pair = section->pairs;
    if (pair)
    {
        while (pair->opaque) pair = (key_value_pair_t*)(pair->opaque);

        pair->opaque = param;
    }
    else
    {
        /** first parameter */
        section->pairs = param;
    }


    section->pair_count++;

    return G_OK;
}

static GS32 iniFileAddSection(my_ini_file_t* mif, ini_section_t* section)
{
    ini_section_t* isec = mif->sections;
    if (isec)
    {
        while (isec->opaque) isec = (ini_section_t*)isec->opaque;

        isec->opaque = section;
    }
    else
    {
        /**first section */
        mif->sections = section;
    }

    mif->section_count++;

    return G_OK;
}

static GBOL iniFileHasSection_l(my_ini_file_t* mif, const char* name)
{
    GBOL ret = GFALSE;
    ini_section_t* isec = mif->sections;

    while (isec)
    {
        if (!strcmp(isec->name, name))
        {
            ret = GTRUE;
            break;
        }

        isec = (ini_section_t*)isec->opaque;
    }
   
    return ret;
}

static ini_section_t* iniFileGetSection_l(my_ini_file_t* mif, const char* name)
{
    ini_section_t* ret  = NULL;
    ini_section_t* isec = mif->sections;

    while (isec)
    {
        if (!strcmp(isec->name, name))
        {
            ret = isec;
            break;
        }

        isec = (ini_section_t*)isec->opaque;
    }

    return ret;
}

static ini_section_t* iniFileGetNewestSection(my_ini_file_t* mif)
{
    ini_section_t* isec = mif->sections;
    if (isec)
    {
        while (isec->opaque) isec = (ini_section_t*)isec->opaque;
    }

    return isec;
}

static char* iniFileParseSection(my_ini_file_t* mif, char* line, 
                                 char* line_end, GU32 line_num, GS32* ret)
{
    char* ptr = line;
    char* pEnd = line_end;
    char* section = strchr(ptr, ']');
    /** process section name */

    if (section)
    {
        //char temp = *section;
        //*section = '\0';

        if (iniFileHasSection_l(mif, ptr) == GTRUE)
        {
            LOGE("syntax error[line:%d]: section(%s) already exist, end parse \n", 
                line_num, ptr);
            ptr = pEnd;
            *ret = G_ErrorUndefined;
        }
        else
        {
            char* pLeft = ptr;
            char* pRight = section - 1;

            /** trim left white-space */
            while (isspace(*pLeft) && *pLeft) pLeft++;

            /** trim right white-space */
            while (isspace(*pRight) && pRight >= pLeft) pRight--;

            *(pRight + 1) = '\0';

            ini_section_t* isec = iniFileNewSection(pLeft);

            if (isec)
            {
                GS32 result = iniFileAddSection(mif, isec);

                if (G_OK == result)
                {
                    ptr = section + 1; /** + 1 indicate skip character ']' */
                }
                else
                {
                    free(isec); /** release resource */
                    LOGE("add secition(%s) to list failed(0x%08x)\n", pLeft, *ret);
                    ptr = pEnd;
                    *ret = result;
                }
            }
            else
            {
                LOGE("create section(%s) failed\n", pLeft);
                ptr = pEnd;
                *ret = G_ErrorInsufficientResources;
            }
        }
    }
    else
    {
        LOGE("syntax error[line:%d]: section doesn't end with ']'\n", line_num);
        ptr = pEnd;
        *ret = G_ErrorUndefined;
    }

    return ptr;
}

static char* iniFileParseParameter(ini_section_t* section, char* line,
                                   char* line_end,GU32 line_num, GS32* ret)
{
    char* ptr = line;
    char* pEnd = line_end;

    char* key_end = strchr(ptr, '=');

    if (key_end)
    {
        char* pRight = key_end - 1;

        /** strip right white-space */
        while (isspace(*pRight) && pRight >= ptr) pRight--;

        GU32 key_len = (pRight + 1) - ptr;
        char* key = ptr;

        char* value = key_end + 1; /** + 1 indicate skip character '=' */

        /** strip left white-space */

        while (isspace(*value) && value < pEnd) value++;


        if (value < pEnd)
        {
            /** find value end */
            char* value_end = value;
  
            /** strip ritght white-space and comment after the value of parameter */
            while ((!isspace(*value_end) && (isCommentTag(*value_end) == GFALSE)) &&
                (value_end < pEnd))
            {
                value_end++;
            }

            if (value_end > value)
            {
                GU32 value_len = value_end - value;

                key_value_pair_t* pair = iniFileNewParameter(key, key_len, value, value_len);

                if (pair)
                {
                    iniFileAddParameter(section, pair);

                    ptr = value_end;
                }
                else
                {
                    ptr = pEnd;
                    *ret = G_ErrorInsufficientResources;
                }
            }
            else
            {
                LOGE("syntax error: the value of parameter is empty(right of \'=\')\n");
                ptr = pEnd;
                *ret = G_ErrorUndefined;
            }

        }
        else
        {
            LOGE("syntax error[line:%d]: the value of parameter is empty(right of \'=\')\n", line_num);
            ptr = pEnd;
            *ret = G_ErrorUndefined;
        }

      
    }
    else
    {
        LOGE("syntax error[line:%d]: not complete key, doesn't find end character \'=\' \n", line_num);
        ptr = pEnd;
        *ret = G_ErrorUndefined;
    }

    return ptr;
}


static GS32 iniFileParseLine(my_ini_file_t* mif, char* line, GU32 length, GU32 line_num)
{
    char* ptr = line;
    char* pEnd = line + length;
    GS32 ret = G_OK;

    while (ptr < pEnd)
    {
        while (isspace(*ptr)) ptr++; /** skip white-space  */

        char ch = *ptr;

        if (isCommentTag(ch) == GTRUE)
        {
            /** skip component */
            ptr = pEnd;
        }
        else if (ch == '[')
        {
            /** ++ indicate skip character '[' */
            ptr = iniFileParseSection(mif, ++ptr, pEnd, line_num, &ret);
        }
        else if (ch == '\0') /** line end */
        {
            /**do nothing */
        }
        else
        {

            if (mif->section_count == 0)
            {
                /** alloc default section */
                ini_section_t* isec = iniFileNewSection(DEFAULT_SECTION_NAME);

                if (isec)
                {

                    ret = iniFileAddSection(mif, isec);

                    if (G_OK != ret)
                    {
                        LOGE("add section to list failed(0x%08x)\n", ret);
                        free(isec);
                        break;
                    }
                }
                else
                {
                    ret = G_ErrorInsufficientResources;
                    break;
                }
            }
           
           /**
            * current section aways is the newest section ?
            * TODO, this case is aways right?
            */
            ptr = iniFileParseParameter(iniFileGetNewestSection(mif),
                                        ptr, pEnd, line_num, &ret);
        }
    }

    return ret;
}

static GS32 iniFileParseFile(my_ini_file_t* mif, FILE* fp)
{
    GS32 ret = G_OK;
   
    GU32 line_num = 0;
    char line_buf[LINE_BUF_MAX_LEN] = { 0 };

    while (fgets(line_buf, LINE_BUF_MAX_LEN, fp))
    {
        line_num++;
        ret = iniFileParseLine(mif, line_buf, strlen(line_buf), line_num);

        if (G_OK != ret)
        {
            LOGE("parse line failed\n");
            break;
        }
    }

    return ret;
}


static GS32 iniFileLoad(tiny_ini_file_t* ini_file, const char* file_name)
{
    GS32 ret = G_ErrorBadParameter;
    my_ini_file_t* mif = (my_ini_file_t*)ini_file;

    if (file_name)
    {
        if (ini_file->is_loaded == GFALSE)
        {
            FILE* fp = fopen(file_name, "r");

            if (fp)
            {
                ret = iniFileParseFile(mif, fp);

                if (G_OK == ret)
                {
                    mif->fp = fp;
                    ini_file->is_loaded = GTRUE;
                }
                else
                {
                    fclose(fp);
                }
            }
            else
            {
                LOGE("open ini file(%s) failed(%d, %s)\n", file_name, errno, strerror(errno));
            }
        }
        else
        {
            LOGE("already load one file, cann't load more\n");
            ret = G_ErrorInvalidOperation;
        }
    }

    return ret;

}

static GS32 iniFileGetValue(struct tiny_ini_file_s* ini_file, const char* section,
                            const char* key, char** value)
{
    GS32 ret = G_ErrorBadParameter;
    my_ini_file_t* mif = (my_ini_file_t*)ini_file;

    if (section && key && value)
    {
        ini_section_t* isec = iniFileGetSection_l(mif, section);

        if (isec)
        {
            if (isec->pairs)
            {
                char* retValue = NULL;
                key_value_pair_t* param = isec->pairs;
                while (param)
                {
                    if (!strcmp(param->key, key))
                    {
                        retValue = param->value;
                        break;
                    }

                    param = (key_value_pair_t*)param->opaque;
                }

                if (retValue)
                {
                    *value = retValue;
                    ret = G_OK;
                }
                else
                {
                    LOGE("not find the parameter which key is(%s), in section(%s)\n",
                        key, section);
                    ret = G_ErrorNotFound;
                }
            }
            else
            {
                LOGE("nothing in this section(%s)\n", section);
                ret = G_ErrorUndefined;
            }
        }
        else
        {
            LOGE("not find secontion(%s)\n", section);
            ret = G_ErrorNotFound;
        }
    }

    return ret;
}



static GS32 iniFileGetValues(struct tiny_ini_file_s* ini_file, const char* section,
                             const char* key, char** values[], GU32* count)
{
    GS32 ret = G_ErrorBadParameter;
    my_ini_file_t* mif = (my_ini_file_t*)ini_file;

    if (section && key && values && count)
    {
        ini_section_t* isec = iniFileGetSection_l(mif, section);

        if (isec)
        {
            if (isec->pairs)
            {
                GU32 getCount = 0;
                GU32 arraySize = 0;
                key_value_pair_t* param = isec->pairs;

                char** valueArray = NULL;

                while (param)
                {
                    if (!strcmp(param->key, key))
                    {
                        if (getCount >= arraySize)
                        {
                            GU32 newSize = arraySize + 4;
                            GU32 elementSize =  sizeof(char*);

                            char** newArray = (char**)realloc(valueArray, newSize * elementSize);

                            if (newArray)
                            {
                                arraySize = newSize;
                                valueArray = newArray;
                            }
                            else
                            {
                                LOGE("realloc value array failed(oldSize:%d, new:%d)\n",
                                    arraySize* elementSize, newSize * elementSize);
                                ret = G_ErrorInsufficientResources;
                                getCount = 0;
                                break;
                            }
                        }

                        valueArray[getCount++] = param->value;
                    }

                    param = (key_value_pair_t*)param->opaque;
                }

                if (getCount)
                {
                    *values = valueArray;
                    *count = getCount;
                    ret = G_OK;
                }
                else
                {
                    LOGE("not find the parameter which key is(%s), in section(%s)\n",
                        key, section);
                    ret = G_ErrorNotFound;
                }

            }
            else
            {
                LOGE("nothing in this section(%s)\n", section);
                ret = G_ErrorUndefined;
            }
        }
        else
        {
            LOGE("not find secontion(%s)\n", section);
            ret = G_ErrorNotFound;
        }
    }

    return ret;
}

static void iniFileFreeValues(struct tiny_ini_file_s* ini_file, char* values[], GU32 count)
{
    free(values);
    /** todo somethings */
}


static GS32 iniFileGetSection(struct tiny_ini_file_s* ini_file, const char* name, ini_section_t** section)
{
    GS32 ret = G_ErrorBadParameter;

    if (name && section)
    {
        *section = iniFileGetSection_l((my_ini_file_t*)ini_file, name);

        ret = *section ? G_OK : G_ErrorNotFound;
    }

    return ret;
}


static GBOL iniFileHasKey(struct tiny_ini_file_s* ini_file, const char* section, const char* key)
{
    GBOL ret = GFALSE;

    if (key && section)
    {
        ini_section_t* isec = iniFileGetSection_l((my_ini_file_t*)ini_file, section);

        if (isec)
        {
            key_value_pair_t* param = isec->pairs;
            while (param)
            {
                if (!strcmp(param->key, key))
                {
                    ret = GTRUE;
                    break;
                }

                param = (key_value_pair_t*)param->opaque;
            }
        }
        else
        {
            LOGE("not find section(%s)\n", section);
        }
    }

    return ret;
}

static GBOL iniFileHasSection(struct tiny_ini_file_s* ini_file, const char* section)
{
    GBOL ret = GFALSE;

    if (section)
    {
        ret = iniFileHasSection_l((my_ini_file_t*)ini_file, section);
    }

    return ret;
}

static void iniFileDump(struct tiny_ini_file_s* ini_file)
{
    my_ini_file_t* mif = (my_ini_file_t*)ini_file;

    ini_section_t* isec = mif->sections;

    while (isec)
    {
        LOGI("\nsection: %s, item_count:%d\n", isec->name, isec->pair_count);
        key_value_pair_t* param = isec->pairs;
        while (param)
        {
            LOGI("%s = %s \n", param->key, param->value);
            param = (key_value_pair_t*)param->opaque;
        }

        isec = (ini_section_t*)isec->opaque;
    }
}

static void iniFileDestroy(struct tiny_ini_file_s* ini_file)
{
    my_ini_file_t* mif = (my_ini_file_t*)ini_file;

    ini_section_t* isec = mif->sections;

    while (isec)
    {
            key_value_pair_t* param = isec->pairs;
            while (param)
            {
                key_value_pair_t* temp = param;
                param = (key_value_pair_t*)param->opaque;

                free(temp); /** free a parameter */
            }
            ini_section_t* temp = isec;
            isec = (ini_section_t*)isec->opaque;

            free(temp); /** free a section */

    }

    if (mif->fp)
    {
        fclose(mif->fp);
    }

    free(mif); /** free ini file instance */
}

//////////////////////////////////////////////////////////////////////////


GS32 TinyIniFileCreate(tiny_ini_file_t** ini_file)
{
    GS32 ret = G_OK;
    my_ini_file_t* mif = (my_ini_file_t*)malloc(sizeof(my_ini_file_t));

    if (mif)
    {
        memset(mif, 0, sizeof(my_ini_file_t));
        tiny_ini_file_t* inf = (tiny_ini_file_t*)mif;
        inf->load       = iniFileLoad;
        inf->getValue   = iniFileGetValue;
        inf->getValues  = iniFileGetValues;
        inf->freeValues = iniFileFreeValues;
        inf->getSection = iniFileGetSection;
        inf->hasKey     = iniFileHasKey;
        inf->hasSection = iniFileHasSection;
        inf->dump       = iniFileDump;
        inf->destroy    = iniFileDestroy;
        inf->is_loaded  = GFALSE;
        *ini_file = inf;
    }
    else
    {
        LOGE("alloc ini file instance failed\n");
        ret = G_ErrorInsufficientResources;
    }

    return ret;
}

